<?php
namespace Wechat\Controller\Component;

use Cake\Controller\Component;

/**
 * 微信消息加密解密
 */
class WxBizMsgCryptComponent extends Component
{

    private $token;

    private $encodingAesKey;

    private $appId;

    private $aes = false;

    /**
     * 构造函数
     *
     * @param $token string
     *            公众平台上，开发者设置的token
     * @param $encodingAesKey string
     *            公众平台上，开发者设置的EncodingAESKey
     * @param $appId string
     *            公众平台的appId
     */
    public function initialize(array $config)
    {
        parent::initialize($config);
        $this->setConfig($config);
        $this->token = $config["token"];
        $this->encodingAesKey = $config["aesKey"];
        $this->appId = $config["appId"];
    }

    public function setAes($aes = true)
    {
        $this->aes = $aes;
    }

   

    // 回复文本消息
    public function transmitText($object, $content)
    {
        $textTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[%s]]></Content>
</xml>";
        $result = sprintf($textTpl, $object->FromUserName, $object->ToUserName, time(), $content);
        return $result;
    }

    // 回复图片消息
    public function transmitImage($object, $imageArray)
    {
        $itemTpl = "<Image>
    <MediaId><![CDATA[%s]]></MediaId>
</Image>";
        
        $item_str = sprintf($itemTpl, $imageArray['MediaId']);
        
        $textTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[image]]></MsgType>
        $item_str
        </xml>";
        
        $result = sprintf($textTpl, $object->FromUserName, $object->ToUserName, time());
        return $result;
    }

    // 回复语音消息
    public function transmitVoice($object, $voiceArray)
    {
        $itemTpl = "<Voice>
			<MediaId><![CDATA[%s]]></MediaId>
</Voice>";
        
        $item_str = sprintf($itemTpl, $voiceArray['MediaId']);
        
        $textTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[voice]]></MsgType>
        $item_str
        </xml>";
        
        $result = sprintf($textTpl, $object->FromUserName, $object->ToUserName, time());
        return $result;
    }

    // 回复视频消息
    public function transmitVideo($object, $videoArray)
    {
        $itemTpl = "<Video>
			<MediaId><![CDATA[%s]]></MediaId>
    <ThumbMediaId><![CDATA[%s]]></ThumbMediaId>
    <Title><![CDATA[%s]]></Title>
    <Description><![CDATA[%s]]></Description>
</Video>";
        
        $item_str = sprintf($itemTpl, $videoArray['MediaId'], $videoArray['ThumbMediaId'], $videoArray['Title'], $videoArray['Description']);
        
        $textTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[video]]></MsgType>
        $item_str
        </xml>";
        
        $result = sprintf($textTpl, $object->FromUserName, $object->ToUserName, time());
        return $result;
    }

    // 回复图文消息
    public function transmitNews($object, $newsArray)
    {
        if (! is_array($newsArray)) {
            return;
        }
        $itemTpl = "<item>
						<Title><![CDATA[%s]]></Title>
        <Description><![CDATA[%s]]></Description>
        <PicUrl><![CDATA[%s]]></PicUrl>
        <Url><![CDATA[%s]]></Url>
    </item>
";
        $item_str = "";
        foreach ($newsArray as $item) {
            $item_str .= sprintf($itemTpl, $item['Title'], $item['Description'], $item['PicUrl'], $item['Url']);
        }
        $newsTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[news]]></MsgType>
        <Content><![CDATA[]]></Content>
        <ArticleCount>%s</ArticleCount>
        <Articles>
        $item_str</Articles>
        </xml>";
        
        $result = sprintf($newsTpl, $object->FromUserName, $object->ToUserName, time(), count($newsArray));
        return $result;
    }

    // 回复音乐消息
    public function transmitMusic($object, $musicArray)
    {
        $itemTpl = "<Music>
    <Title><![CDATA[%s]]></Title>
    <Description><![CDATA[%s]]></Description>
    <MusicUrl><![CDATA[%s]]></MusicUrl>
    <HQMusicUrl><![CDATA[%s]]></HQMusicUrl>
	    </Music>";
        
        $item_str = sprintf($itemTpl, $musicArray['Title'], $musicArray['Description'], $musicArray['MusicUrl'], $musicArray['HQMusicUrl']);
        
        $textTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[music]]></MsgType>
        $item_str
        </xml>";
        
        $result = sprintf($textTpl, $object->FromUserName, $object->ToUserName, time());
        return $result;
    }

    /**
     * 将公众平台回复用户的消息加密打包.
     * <ol>
     * <li>对要发送的消息进行AES-CBC加密</li>
     * <li>生成安全签名</li>
     * <li>将消息密文和安全签名打包成xml格式</li>
     * </ol>
     *
     * @param $replyMsg string
     *            公众平台待回复用户的消息，xml格式的字符串
     * @param $timeStamp string
     *            时间戳，可以自己生成，也可以用URL参数的timestamp
     * @param $nonce string
     *            随机串，可以自己生成，也可以用URL参数的nonce
     * @param
     *            &$encryptMsg string 加密后的可以直接回复用户的密文，包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
     *            当return返回0时有效
     *            
     * @return int 成功0，失败返回对应的错误码
     */
    public function encryptMsg($replyMsg, $timeStamp=null, $nonce=null)
    {
        $this->Prpcrypt($this->encodingAesKey);
        
        // 加密
        $array = $this->encrypt($replyMsg, $this->appId);
        $ret = $array[0];
        if ($ret != 0) {
            return $ret;
        }
        
        if ($timeStamp == null) {
            $timeStamp = time();
        }
        if ($nonce == null) {
            $nonce = $this->getRandomStr();
        }
        $encrypt = $array[1];
        
        // 生成安全签名
        $array = $this->getSHA1($this->token, $timeStamp, $nonce, $encrypt);
        $ret = $array[0];
        if ($ret != 0) {
            return $ret;
        }
        $signature = $array[1];
        
        // 生成发送的xml
        return $this->generate($encrypt, $signature, $timeStamp, $nonce);
    }

    /**
     * 检验消息的真实性，并且获取解密后的明文.
     * <ol>
     * <li>利用收到的密文生成安全签名，进行签名验证</li>
     * <li>若验证通过，则提取xml中的加密消息</li>
     * <li>对消息进行解密</li>
     * </ol>
     *
     * @param $msgSignature string
     *            签名串，对应URL参数的msg_signature
     * @param $timestamp string
     *            时间戳 对应URL参数的timestamp
     * @param $nonce string
     *            随机串，对应URL参数的nonce
     * @param $postData string
     *            密文，对应POST请求的数据
     * @param
     *            &$msg string 解密后的原文，当return返回0时有效
     *            
     * @return int 成功0，失败返回对应的错误码
     */
    public function decryptMsg($msgSignature, $timestamp = null, $nonce, $postData)
    {
        if (strlen($this->encodingAesKey) != 43) {
            return $this->IllegalAesKey;
        }
        $this->aes = true;
        $this->Prpcrypt($this->encodingAesKey);
        
        if ($timestamp == null) {
            $timestamp = time();
        }
        
        $encrypt = $postData["Encrypt"];
        $touser_name = $postData["ToUserName"];
        
        // 验证安全签名
        $array = $this->getSHA1($this->token, $timestamp, $nonce, $encrypt);
        $ret = $array[0];
        
        if ($ret != 0) {
            return $ret;
        }
        
        $signature = $array[1];
        if ($signature != $msgSignature) {
            return $this->ValidateSignatureError;
        }
        
        $result = $this->decrypt($encrypt, $this->appId);
        if ($result[0] != 0) {
            return $result[0];
        }
        return $this->formartXml($result[1]);
    }

    /**
     * 对明文进行加密
     *
     * @param string $text
     *            需要加密的明文
     * @return string 加密后的密文
     */
    public function encrypt($text, $appid)
    {
        try {
            // 获得16位随机字符串，填充到明文之前
            $random = $this->getRandomStr();
            $text = $random . pack("N", strlen($text)) . $text . $appid;
            // 网络字节序
            $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
            $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
            $iv = substr($this->key, 0, 16);
            // 使用自定义的填充方式对明文进行补位填充
            $text = $this->encode($text);
            mcrypt_generic_init($module, $this->key, $iv);
            // 加密
            $encrypted = mcrypt_generic($module, $text);
            mcrypt_generic_deinit($module);
            mcrypt_module_close($module);
            
            // print(base64_encode($encrypted));
            // 使用BASE64对加密后的字符串进行编码
            return array(
                $this->OK,
                base64_encode($encrypted)
            );
        } catch (\Exception $e) {
            // print $e;
            return array(
                $this->EncryptAESError,
                null
            );
        }
    }

    /**
     * 对密文进行解密
     *
     * @param string $encrypted
     *            需要解密的密文
     * @return string 解密得到的明文
     */
    public function decrypt($encrypted, $appid)
    {
        try {
            // 使用BASE64对需要解密的字符串进行解码
            $ciphertext_dec = base64_decode($encrypted);
            $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
            $iv = substr($this->key, 0, 16);
            mcrypt_generic_init($module, $this->key, $iv);
            
            // 解密
            $decrypted = mdecrypt_generic($module, $ciphertext_dec);
            mcrypt_generic_deinit($module);
            mcrypt_module_close($module);
        } catch (\Exception $e) {
            return array(
                $this->DecryptAESError,
                null
            );
        }
        
        try {
            // 去除补位字符
            $result = $this->decode($decrypted);
            // 去除16位随机字符串,网络字节序和AppId
            if (strlen($result) < 16)
                return "";
            $content = substr($result, 16, strlen($result));
            $len_list = unpack("N", substr($content, 0, 4));
            $xml_len = $len_list[1];
            $xml_content = substr($content, 4, $xml_len);
            $from_appid = substr($content, $xml_len + 4);
        } catch (\Exception $e) {
            // print $e;
            return array(
                $this->IllegalBuffer,
                null
            );
        }
        if ($from_appid != $appid)
            return array(
                $this->ValidateAppidError,
                null
            );
        return array(
            0,
            $xml_content
        );
    }

    function Prpcrypt($k)
    {
        $this->key = base64_decode($k . "=");
    }

    public $block_size = 32;

    /**
     * 对需要加密的明文进行填充补位
     *
     * @param $text 需要进行填充补位操作的明文            
     * @return 补齐明文字符串
     */
    function encode($text)
    {
        $block_size = $this->block_size;
        $text_length = strlen($text);
        // 计算需要填充的位数
        $amount_to_pad = $this->block_size - ($text_length % $this->block_size);
        if ($amount_to_pad == 0) {
            $amount_to_pad = $this->block_size;
        }
        // 获得补位所用的字符
        $pad_chr = chr($amount_to_pad);
        $tmp = "";
        for ($index = 0; $index < $amount_to_pad; $index ++) {
            $tmp .= $pad_chr;
        }
        return $text . $tmp;
    }

    /**
     * 对解密后的明文进行补位删除
     *
     * @param
     *            decrypted 解密后的明文
     * @return 删除填充补位后的明文
     */
    function decode($text)
    {
        $pad = ord(substr($text, - 1));
        if ($pad < 1 || $pad > 32) {
            $pad = 0;
        }
        return substr($text, 0, (strlen($text) - $pad));
    }

    /**
     * 提取出xml数据包中的加密消息
     *
     * @param string $xmltext
     *            待提取的xml字符串
     * @return string 提取出的加密消息字符串
     */
    public function formartXml($xmltext)
    {
        try {
            libxml_disable_entity_loader(true);
            $values = json_decode(json_encode(simplexml_load_string($xmltext, 'SimpleXMLElement', LIBXML_NOCDATA)));
            return $values;
        } catch (\Exception $e) {
            // print $e . "\n";
            return [];
        }
    }

    /**
     * 生成xml消息
     *
     * @param string $encrypt
     *            加密后的消息密文
     * @param string $signature
     *            安全签名
     * @param string $timestamp
     *            时间戳
     * @param string $nonce
     *            随机字符串
     */
    public function generate($encrypt, $signature, $timestamp, $nonce)
    {
        $format = "<xml>
<Encrypt><![CDATA[%s]]></Encrypt>
<MsgSignature><![CDATA[%s]]></MsgSignature>
<TimeStamp>%s</TimeStamp>
<Nonce><![CDATA[%s]]></Nonce>
</xml>";
        return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
    }

    /**
     * 随机生成16位字符串
     *
     * @return string 生成的字符串
     */
    public function getRandomStr()
    {
        $str = "";
        $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
        $max = strlen($str_pol) - 1;
        for ($i = 0; $i < 16; $i ++) {
            $str .= $str_pol[mt_rand(0, $max)];
        }
        return $str;
    }

    /**
     * 用SHA1算法生成安全签名
     *
     * @param string $token
     *            票据
     * @param string $timestamp
     *            时间戳
     * @param string $nonce
     *            随机字符串
     * @param string $encrypt
     *            密文消息
     */
    public function getSHA1($token, $timestamp, $nonce, $encrypt_msg)
    {
        // 排序
        try {
            $array = array(
                $encrypt_msg,
                $token,
                $timestamp,
                $nonce
            );
            sort($array, SORT_STRING);
            $str = implode($array);
            return array(
                $this->OK,
                sha1($str)
            );
        } catch (\Exception $e) {
            return array(
                $this->ComputeSignatureError,
                null
            );
        }
    }

    public $key;

    public $OK = 0;

    public $ValidateSignatureError = - 40001;

    public $ParseXmlError = - 40002;

    public $ComputeSignatureError = - 40003;

    public $IllegalAesKey = - 40004;

    public $ValidateAppidError = - 40005;

    public $EncryptAESError = - 40006;

    public $DecryptAESError = - 40007;

    public $IllegalBuffer = - 40008;

    public $EncodeBase64Error = - 40009;

    public $DecodeBase64Error = - 40010;

    public $GenReturnXmlError = - 40011;
}
